// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: qjs_element.cpp
// Description: Bindings for Element
//      Author: Ziming Li
//     Created: 2022-03-08
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "./qjs_bindings.h"

#include "blinkit/blink/renderer/bindings/core/qjs/qjs.h"
#include "blinkit/blink/renderer/bindings/core/qjs/qjs_c_function_list_entries.h"
#include "blinkit/blink/renderer/bindings/core/script_controller.h"
#include "blinkit/blink/renderer/core/dom/dom_token_list.h"
#ifdef BLINKIT_UI_ENABLED
#   include "blinkit/blink/renderer/core/css/css_style_declaration.h"
#   include "blinkit/blink/renderer/core/dom/client_rect.h"
#   include "blinkit/blink/renderer/core/dom/shadow/ShadowRoot.h"
#   include "blinkit/blink/renderer/core/dom/shadow/shadow_root_init.h"
#   include "blinkit/blink/renderer/core/html/HTMLElement.h"
#else
#   include "blinkit/blink/renderer/core/dom/element.h"
#endif

/**
 * https://developer.mozilla.org/en-US/docs/Web/API/Element
 */

namespace BlinKit {
namespace qjs {

namespace Strings {
static const char ClassList[] = "classList";
static const char FirstElementChild[] = "firstElementChild";
static const char GetAttribute[] = "getAttribute";
static const char HasAttribute[] = "hasAttribute";
static const char Id[] = "id";
static const char InnerHTML[] = "innerHTML";
static const char InnerText[] = "innerText";
static const char InvalidAttributeName[] = "Invalid attribute name";
static const char LastElementChild[] = "lastElementChild";
static const char NextElementSibling[] = "nextElementSibling";
static const char OuterHTML[] = "outerHTML";
static const char PreviousElementSibling[] = "previousElementSibling";
static const char RemoveAttribute[] = "removeAttribute";
static const char SetAttribute[] = "setAttribute";
static const char TagName[] = "tagName";
}

static JSValue GetAttribute(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, Strings::InvalidAttributeName);

    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    const AtomicString &ret = element->getAttribute(ToAtomicString(ctx, argv[0]));
    return WrapString(ctx, ret);
}

static JSValue GetClassList(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return ScriptController::From(ctx)->Wrap<DOMTokenList>(&(element->classList()));
}

static JSValue GetFirstElementChild(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Element *ret = nullptr;
    for (Node *node = element->firstChild(); nullptr != node; node = node->nextSibling())
    {
        if (node->isElementNode())
        {
            ret = toElement(node);
            break;
        }
    }
    return ScriptController::From(ctx)->WrapElement(ret);
}

static JSValue GetId(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->getIdAttribute());
}

static JSValue GetInnerHTML(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->innerHTML());
}

static JSValue GetInnerText(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->innerText());
}

static JSValue GetLastElementChild(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Element *ret = nullptr;
    for (Node *node = element->lastChild(); nullptr != node; node = node->previousSibling())
    {
        if (node->isElementNode())
        {
            ret = toElement(node);
            break;
        }
    }
    return ScriptController::From(ctx)->WrapElement(ret);
}

static JSValue GetNextElementSibling(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Element *ret = nullptr;
    for (Node *node = element->nextSibling(); nullptr != node; node = node->nextSibling())
    {
        if (node->isElementNode())
        {
            ret = toElement(node);
            break;
        }
    }
    return ScriptController::From(ctx)->WrapElement(ret);
}

static JSValue GetOuterHTML(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->outerHTML());
}

static JSValue GetPreviousElementSibling(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Element *ret = nullptr;
    for (Node *node = element->previousSibling(); nullptr != node; node = node->previousSibling())
    {
        if (node->isElementNode())
        {
            ret = toElement(node);
            break;
        }
    }
    return ScriptController::From(ctx)->WrapElement(ret);
}

static JSValue GetTagName(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->tagName());
}

static JSValue HasAttribute(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, Strings::InvalidAttributeName);

    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    bool ret = element->hasAttribute(ToAtomicString(ctx, argv[0]));
    return JS_NewBool(ctx, ret);
}

static JSValue RemoveAttribute(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, Strings::InvalidAttributeName);

    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    element->removeAttribute(ToAtomicString(ctx, argv[0]));
    return JS_UNDEFINED;
}

static JSValue SetAttribute(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsString(argv[0]))
        return JS_ThrowTypeError(ctx, Strings::InvalidAttributeName);

    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    const AtomicString name = ToAtomicString(ctx, argv[0]);
    AtomicString value;
    if (JS_IsString(argv[1]))
    {
        value = ToAtomicString(ctx, argv[1]);
    }
    else if (JS_IsNumber(argv[1]))
    {
        JSValue v = JS_ToString(ctx, argv[1]);
        value = ToAtomicString(ctx, v);
        JS_FreeValue(ctx, v);
    }
    else
    {
        return JS_ThrowTypeError(ctx, "Invalid attribute value");
    }

    Context c(*ctx);
    element->setAttribute(name, value, c);
    if (c.hadException())
        return c.ReturnException();

    return JS_UNDEFINED;
}

static JSValue SetId(JSContext *ctx, JSValueConst thisVal, JSValueConst id)
{
    if (!JS_IsString(id))
        return JS_ThrowTypeError(ctx, "Invalid id!");

    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Context c(*ctx);
    element->setAttribute(HTMLNames::idAttr, ToAtomicString(ctx, id));

    return JS_UNDEFINED;
}

static JSValue SetInnerHTML(JSContext *ctx, JSValueConst thisVal, JSValueConst html)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Context c(*ctx);
    element->setInnerHTML(ToAtomicString(ctx, html), c);
    if (c.hadException())
        return c.ReturnException();

    return JS_UNDEFINED;
}

static JSValue SetOuterHTML(JSContext *ctx, JSValueConst thisVal, JSValueConst html)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Context c(*ctx);
    element->setOuterHTML(ToAtomicString(ctx, html), c);
    if (c.hadException())
        return c.ReturnException();

    return JS_UNDEFINED;
}

#ifdef BLINKIT_CRAWLER_ENABLED
JSValue CreateElementPrototypeForCrawler(JSContext *ctx, JSValue containerNodePrototype)
{
    static const JSCFunctionListEntry Funcs[] = {
        CGetSetEntry(Strings::ClassList, GetClassList, nullptr),
        CGetSetEntry(Strings::FirstElementChild, GetFirstElementChild, nullptr),
        CFunctionEntry(Strings::GetAttribute, 1, GetAttribute),
        CFunctionEntry(Strings::HasAttribute, 1, HasAttribute),
        CGetSetEntry(Strings::Id, GetId, SetId),
        CGetSetEntry(Strings::InnerHTML, GetInnerHTML, SetInnerHTML),
        CGetSetEntry(Strings::InnerText, GetInnerText, nullptr),
        CGetSetEntry(Strings::LastElementChild, GetLastElementChild, nullptr),
        CGetSetEntry(Strings::NextElementSibling, GetNextElementSibling, nullptr),
        CGetSetEntry(Strings::OuterHTML, GetOuterHTML, SetOuterHTML),
        CGetSetEntry(Strings::PreviousElementSibling, GetPreviousElementSibling, nullptr),
        CFunctionEntry(Strings::RemoveAttribute, 1, RemoveAttribute),
        CFunctionEntry(Strings::SetAttribute, 2, SetAttribute),
        CGetSetEntry(Strings::TagName, GetTagName, nullptr)
    };

    JSValue ret = JS_NewObjectProto(ctx, containerNodePrototype);
    JS_SetPropertyFunctionList(ctx, ret, Funcs, std::size(Funcs));
    return ret;
}
#endif

#ifdef BLINKIT_UI_ENABLED
namespace UI {

static ShadowRootInit ToShadowRootInit(JSContext *ctx, JSValueConst src)
{
    ShadowRootInit ret;

    JSValue v = JS_GetPropertyStr(ctx, src, "mode");
    if (JS_IsString(v))
        ret.SetMode(ToStdString(ctx, v));
    JS_FreeValue(ctx, v);

    v = JS_GetPropertyStr(ctx, src, "delegatesFocus");
    if (JS_IsBool(v))
        ret.SetDelegatesFocus(JS_ToBool(ctx, v));
    JS_FreeValue(ctx, v);

    return ret;
}

static JSValue AttachShadow(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    if (!JS_IsObject(argv[0]))
        return JS_ThrowTypeError(ctx, "Invalid init value");

    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    Context c(*ctx);
    GCRefPtr<ShadowRoot> ret = element->attachShadow(ToShadowRootInit(ctx, argv[0]), c);
    if (c.hadException())
        return c.ReturnException();

    return ScriptController::From(ctx)->Wrap(ret.get());
}

static JSValue Focus(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    element->focus();
    return JS_UNDEFINED;
}

static JSValue GetBoundingClientRect(JSContext *ctx, JSValueConst thisVal, int, JSValueConst *)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    GCRefPtr<ClientRect> ret = element->getBoundingClientRect();
    return ScriptController::From(ctx)->Wrap(ret.get());
}

static JSValue GetClassName(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->getClassAttribute());
}

static JSValue GetClientHeight(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->clientHeight());
}

static JSValue GetClientWidth(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->clientWidth());
}

static JSValue GetOffsetHeight(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->offsetHeight());
}

static JSValue GetOffsetLeft(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->offsetLeft());
}

static JSValue GetOffsetTop(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->offsetTop());
}

static JSValue GetOffsetWidth(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewInt32(ctx, element->offsetWidth());
}

static JSValue GetStyle(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return ScriptController::From(ctx)->Wrap(element->style());
}

static JSValue GetScrollTop(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return JS_NewFloat64(ctx, element->scrollTop());
}

static JSValue GetTitle(JSContext *ctx, JSValueConst thisVal)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    return WrapString(ctx, element->title());
}

static JSValue ScrollIntoView(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    ASSERT(JS_IsBool(argv[0]));
    element->scrollIntoView(JS_ToBool(ctx, argv[0]));
    return JS_UNDEFINED;
}

static JSValue SetClassName(JSContext *ctx, JSValueConst thisVal, JSValueConst className)
{
    if (!JS_IsString(className))
        return JS_ThrowTypeError(ctx, "Invalid className value");

    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    element->setAttribute(HTMLNames::classAttr, ToAtomicString(ctx, className));
    return JS_UNDEFINED;
}

static JSValue SetInnerText(JSContext *ctx, JSValueConst thisVal, JSValueConst text)
{
    HTMLElement *element = ScriptWrappable::Cast<HTMLElement>(thisVal);

    Context c(*ctx);
    element->setInnerText(ToAtomicString(ctx, text), c);
    if (c.hadException())
        return c.ReturnException();

    return JS_UNDEFINED;
}

static JSValue SetStyle(JSContext *ctx, JSValueConst thisVal, JSValueConst style)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    ASSERT(false); // BKTODO:
    return JS_UNDEFINED;
}

static JSValue SetScrollTop(JSContext *ctx, JSValueConst thisVal, JSValueConst value)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);

    double d = 0.;
    JS_ToFloat64(ctx, &d, value);
    element->setScrollTop(d);

    return JS_UNDEFINED;
}

static JSValue SetTitle(JSContext *ctx, JSValueConst thisVal, JSValueConst title)
{
    Element *element = ScriptWrappable::Cast<Element>(thisVal);
    element->setAttribute(HTMLNames::titleAttr, ToAtomicString(ctx, title));
    return JS_UNDEFINED;
}

} // namespace UI

JSValue CreateElementPrototypeForUI(JSContext *ctx, JSValue containerNodePrototype)
{
    static const JSCFunctionListEntry Funcs[] = {
        CFunctionEntry("attachShadow", 1, UI::AttachShadow),
        CGetSetEntry(Strings::ClassList, GetClassList, nullptr),
        CGetSetEntry("className", UI::GetClassName, UI::SetClassName),
        CGetSetEntry("clientHeight", UI::GetClientHeight, nullptr),
        CGetSetEntry("clientWidth", UI::GetClientWidth, nullptr),
        CGetSetEntry(Strings::FirstElementChild, GetFirstElementChild, nullptr),
        CFunctionEntry("focus", 0, UI::Focus),
        CFunctionEntry(Strings::GetAttribute, 1, GetAttribute),
        CFunctionEntry("getBoundingClientRect", 0, UI::GetBoundingClientRect),   
        CFunctionEntry(Strings::HasAttribute, 1, HasAttribute),
        CGetSetEntry(Strings::Id, GetId, SetId),
        CGetSetEntry(Strings::InnerHTML, GetInnerHTML, SetInnerHTML),
        CGetSetEntry(Strings::InnerText, GetInnerText, UI::SetInnerText),
        CGetSetEntry(Strings::LastElementChild, GetLastElementChild, nullptr),
        CGetSetEntry(Strings::NextElementSibling, GetNextElementSibling, nullptr),
        CGetSetEntry("offsetHeight", UI::GetOffsetHeight, nullptr),
        CGetSetEntry("offsetLeft", UI::GetOffsetLeft, nullptr),
        CGetSetEntry("offsetTop", UI::GetOffsetTop, nullptr),
        CGetSetEntry("offsetWidth", UI::GetOffsetWidth, nullptr),
        CGetSetEntry(Strings::OuterHTML, GetOuterHTML, SetOuterHTML),
        CGetSetEntry(Strings::PreviousElementSibling, GetPreviousElementSibling, nullptr),
        CFunctionEntry(Strings::RemoveAttribute, 1, RemoveAttribute),
        CFunctionEntry("scrollIntoView", 1, UI::ScrollIntoView),
        CGetSetEntry("scrollTop", UI::GetScrollTop, UI::SetScrollTop),
        CFunctionEntry(Strings::SetAttribute, 2, SetAttribute),
        CGetSetEntry("style", UI::GetStyle, UI::SetStyle),
        CGetSetEntry(Strings::TagName, GetTagName, nullptr),
        CGetSetEntry("title", UI::GetTitle, UI::SetTitle)
    };

    JSValue ret = JS_NewObjectProto(ctx, containerNodePrototype);
    JS_SetPropertyFunctionList(ctx, ret, Funcs, std::size(Funcs));
    return ret;
}
#endif

} // namespace qjs
} // namespace BlinKit
